[Chapter Eleven][Previous]
[Next] [Art of
Assembly][Randall Hyde]
Art of Assembly: Chapter Eleven
- 11.5.11 - Passing Parameters via a Parameter
Block
- 11.6 - Function Results
- 11.6.1 - Returning Function Results in
a Register
- 11.6.2 - Returning Function Results on
the Stack
- 11.6.3 - Returning Function Results in
Memory Locations
- 11.7 - Side Effects
11.5.11 Passing Parameters via a Parameter Block
Another way to pass parameters in memory is through a parameter block.
A parameter block is a set of contiguous memory locations containing the
parameters. To access such parameters, you would pass the subroutine a pointer
to the parameter block. Consider the subroutine from the previous section
that adds J and K together, storing the result in I; the code that passes
these parameters through a parameter block might be
Calling sequence:
ParmBlock dword I
I word ? ;I, J, and K must appear in
J word ? ; this order.
K word ?
.
.
.
les bx, ParmBlock
call AddEm
.
.
.
AddEm proc near
push ax
mov ax, es:2[bx] ;Get J's value
add ax, es:4[bx] ;Add in K's value
mov es:[bx], ax ;Store result in I
pop ax
ret
AddEm endp
Note that you must allocate the three parameters in contiguous memory locations.
This form of parameter passing works well when passing several parameters
by reference, because you can initialize pointers to the parameters directly
within the assembler. For example, suppose you wanted to create a subroutine
rotate
to which you pass four parameters by reference. This
routine would copy the second parameter to the first, the third to the second,
the fourth to the third, and the first to the fourth. Any easy way to accomplish
this in assembly is
; Rotate- On entry, BX points at a parameter block in the data
; segment that points at four far pointers. This code
; rotates the data referenced by these pointers.
Rotate proc near
push es ;Need to preserve these
push si ; registers
push ax
les si, [bx+4] ;Get ptr to 2nd var
mov ax, es:[si] ;Get its value
les si, [bx] ;Get ptr to 1st var
xchg ax, es:[si] ;2nd->1st, 1st->ax
les si, [bx+12] ;Get ptr to 4th var
xchg ax, es:[si] ;1st->4th, 4th->ax
les si, [bx+8] ;Get ptr to 3rd var
xchg ax, es:[si] ;4th->3rd, 3rd->ax
les si, [bx+4] ;Get ptr to 2nd var
mov es:[si], ax ;3rd -> 2nd
pop ax
pop si
pop es
ret
Rotate endp
To call this routine, you pass it a pointer to a group of four far pointers
in the bx register. For example, suppose you wanted to rotate the first
elements of four different arrays, the second elements of those four arrays,
and the third elements of those four arrays. You could do this with the
following code:
lea bx, RotateGrp1
call Rotate
lea bx, RotateGrp2
call Rotate
lea bx, RotateGrp3
call Rotate
.
.
.
RotateGrp1 dword ary1[0], ary2[0], ary3[0], ary4[0]
RotateGrp2 dword ary1[2], ary2[2], ary3[2], ary4[2]
RotateGrp3 dword ary1[4], ary2[4], ary3[4], ary4[4]
Note that the pointer to the parameter block is itself a parameter. The
examples in this section pass this pointer in the registers. However, you
can pass this pointer anywhere you would pass any other reference parameter
- in registers, in global variables, on the stack, in the code stream, even
in another parameter block! Such variations on the theme, however, will
be left to your own imagination. As with any parameter, the best place to
pass a pointer to a parameter block is in the registers. This text will
generally adopt that policy.
Although beginning assembly language programmers rarely use parameter blocks,
they certainly have their place. Some of the IBM PC BIOS and MS-DOS functions
use this parameter passing mechanism. Parameter blocks, since you can initialize
their values during assembly (using byte
, word
,
etc.), provide a fast, efficient way to pass parameters to a procedure.
Of course, you can pass parameters by value, reference, value-returned,
result, or by name in a parameter block. The following piece of code is
a modification of the Rotate
procedure above where the first
parameter is passed by value (its value appears inside the parameter block),
the second is passed by reference, the third by value-returned, and the
fourth by name (there is no pass by result since Rotate
needs
to read and write all values). For simplicity, this code uses near pointers
and assumes all variables appear in the data segment:
; Rotate- On entry, DI points at a parameter block in the data
; segment that points at four pointers. The first is
; a value parameter, the second is passed by reference,
; the third is passed by value/return, the fourth is
; passed by name.
Rotate proc near
push si ;Used to access ref parms
push ax ;Temporary
push bx ;Used by pass by name parm
push cx ;Local copy of val/ret parm
mov si, [di+4] ;Get a copy of val/ret parm
mov cx, [si]
mov ax, [di] ;Get 1st (value) parm
call word ptr [di+6] ;Get ptr to 4th var
xchg ax, [bx] ;1st->4th, 4th->ax
xchg ax, cx ;4th->3rd, 3rd->ax
mov bx, [di+2] ;Get adrs of 2nd (ref) parm
xchg ax, [bx] ;3rd->2nd, 2nd->ax
mov [di], ax ;2nd->1st
mov bx, [di+4] ;Get ptr to val/ret parm
mov [bx], cx ;Save val/ret parm away.
pop cx
pop bx
pop ax
pop si
ret
Rotate endp
A reasonable example of a call to this routine might be:
I word 10
J word 15
K word 20
RotateBlk word 25, I, J, KThunk
.
.
.
lea di, RotateBlk
call Rotate
.
.
.
KThunk proc near
lea bx, K
ret
KThunk endp
11.6 Function Results
Functions return a result, which is nothing more than a result parameter.
In assembly language, there are very few differences between a procedure
and a function. That is probably why there aren't any "func
"
or "endf
" directives. Functions and procedures are
usually different in HLLs, function calls appear only in expressions, subroutine
calls as statements[7]. Assembly language doesn't
distinguish between them.
You can return function results in the same places you pass and return parameters.
Typically, however, a function returns only a single value (or single data
structure) as the function result. The methods and locations used to return
function results is the subject of the next three sections.
11.6.1 Returning Function Results in a Register
Like parameters, the 80x86's registers are the best place to return
function results. The getc
routine in the UCR Standard Library
is a good example of a function that returns a value in one of the CPU's
registers. It reads a character from the keyboard and returns the ASCII
code for that character in the al
register. Generally, functions
return their results in the following registers:
Use First Last
Bytes: al, ah, dl, dh, cl, ch, bl, bh
Words: ax, dx, cx, si, di, bx
Double words: dx:ax On pre-80386
eax, edx, ecx, esi, edi, ebx On 80386 and later.
16-bitOffsets: bx, si, di, dx
32-bit Offsets ebx, esi , edi, eax, ecx, edx
Segmented Pointers: es:di, es:bx, dx:ax, es:si Do not use DS.
Once again, this table represents general guidelines. If you're so inclined,
you could return a double word value in (cl, dh, al, bh
). If
you're returning a function result in some registers, you shouldn't save
and restore those registers. Doing so would defeat the whole purpose of
the function.
11.6.2 Returning Function Results on the Stack
Another good place where you can return function results is on the stack.
The idea here is to push some dummy values onto the stack to create space
for the function result. The function, before leaving, stores its result
into this location. When the function returns to the caller, it pops everything
off the stack except this function result. Many HLLs use this technique
(although most HLLs on the IBM PC return function results in the registers).
The following code sequences show how values can be returned on the stack:
function PasFunc(i,j,k:integer):integer;
begin
PasFunc := i+j+k;
end;
m := PasFunc(2,n,l);
In assembly:
PasFunc_rtn equ 10[bp]
PasFunc_i equ 8[bp]
PasFunc_j equ 6[bp]
PasFunc_k equ 4[bp]
PasFunc proc near
push bp
mov bp, sp
push ax
mov ax, PasFunc_i
add ax, PasFunc_j
add ax, PasFunc_k
mov PasFunc_rtn, ax
pop ax
pop bp
ret 6
PasFunc endp
Calling sequence:
push ax ;Space for function return result
mov ax, 2
push ax
push n
push l
call PasFunc
pop ax ;Get function return result
On an 80286 or later processor you could also use the code:
push ax ;Space for function return result
push 2
push n
push l
call PasFunc
pop ax ;Get function return result
Although the caller pushed eight bytes of data onto the stack, PasFunc only
removes six. The first "parameter" on the stack is the function
result. The function must leave this value on the stack when it returns.
11.6.3 Returning Function Results in Memory Locations
Another reasonable place to return function results is in a known memory
location. You can return function values in global variables or you can
return a pointer (presumably in a register or a register pair) to a parameter
block. This process is virtually identical to passing parameters to a procedure
or function in global variables or via a parameter block.
Returning parameters via a pointer to a parameter block is an excellent
way to return large data structures as function results. If a function returns
an entire array, the best way to return this array is to allocate some storage,
store the data into this area, and leave it up to the calling routine to
deallocate the storage. Most high level languages that allow you to return
large data structures as function results use this technique.
Of course, there is very little difference between returning a function
result in memory and the pass by result parameter passing mechanism. See
"Pass by Result" on page 576
for more details.
11.7 Side Effects
A side effect is any computation or operation by a procedure that isn't
the primary purpose of that procedure. For example, if you elect not to
preserve all affected registers within a procedure, the modification of
those registers is a side effect of that procedure. Side effect programming,
that is, the practice of using a procedure's side effects, is very dangerous.
All too often a programmer will rely on a side effect of a procedure. Later
modifications may change the side effect, invalidating all code relying
on that side effect. This can make your programs hard to debug and maintain.
Therefore, you should avoid side effect programming.
Perhaps some examples of side effect programming will help enlighten you
to the difficulties you may encounter. The following procedure zeros out
an array. For efficiency reasons, it makes the caller responsible for preserving
necessary registers. As a result, one side effect of this procedure is that
the bx
and cx
registers are modified. In particular,
the cx
register contains zero upon return.
ClrArray proc near
lea bx, array
mov cx, 32
ClrLoop: mov word ptr [bx], 0
inc bx
inc bx
loop ClrLoop
ret
ClrArray endp
If your code expects cx
to contain zero after the execution
of this subroutine, you would be relying on a side effect of the ClrArray
procedure. The main purpose behind this code is zeroing out an array, not
setting the cx
register to zero. Later, if you modify the ClrArray
procedure to the following, your code that depends upon cx
containing zero would no longer work properly:
ClrArray proc near
lea bx, array
ClrLoop: mov word ptr [bx], 0
inc bx
inc bx
cmp bx, offset array+32
jne ClrLoop
ret
ClrArray endp
So how can you avoid the pitfalls of side effect programming in your procedures?
By carefully structuring your code and paying close attention to exactly
how your calling code and the subservient procedures interface with one
another. These rules can help you avoid problems with side effect programming:
- Always properly document the input and output conditions of a procedure.
Never rely on any other entry or exit conditions other than these documented
operations.
- Partition your procedures so that they compute a single value or execute
a single operation. Subroutines that do two or more tasks are, by definition,
producing side effects unless every invocation of that subroutine requires
all the computations and operations.
- When updating the code in a procedure, make sure that it still obeys
the entry and exit conditions. If not, either modify the program so that
it does or update the documentation for that procedure to reflect the new
entry and exit conditions.
- Avoid passing information between routines in the CPU's flag register.
Passing an error status in the carry flag is about as far as you should
ever go. Too many instructions affect the flags and it's too easy to foul
up a return sequence so that an important flag is modified on return.
- Always save and restore all registers a procedure modifies.
- Avoid passing parameters and function results in global variables.
- Avoid passing parameters by reference (with the intent of modifying
them for use by the calling code).
These rules, like all other rules, were meant to be broken. Good programming
practices are often sacrificed on the altar of efficiency. There is nothing
wrong with breaking these rules as often as you feel necessary. However,
your code will be difficult to debug and maintain if you violate these rules
often. But such is the price of efficiency[8].
Until you gain enough experience to make a judicious choice about the use
of side effects in your programs, you should avoid them. More often than
not, the use of a side effect will cause more problems than it solves.
[7] "C" is an exception to this rule.
C's procedures and functions are all called functions. PL/I is another exception.
In PL/I, they're all called procedures.
[8]
This is not just a snide remark. Expert programmers who have to wring the
last bit of performance out of a section of code often resort to poor programming
practices in order to achieve their goals. They are prepared, however, to
deal with the problems that are often encountered in such situations and
they are a lot more careful when dealing with such code.
- 11.5.11 - Passing Parameters via a Parameter
Block
- 11.6 - Function Results
- 11.6.1 - Returning Function Results in
a Register
- 11.6.2 - Returning Function Results on
the Stack
- 11.6.3 - Returning Function Results in
Memory Locations
- 11.7 - Side Effects
Art of Assembly: Chapter Eleven - 27 SEP 1996
[Chapter Eleven][Previous]
[Next] [Art of
Assembly][Randall Hyde]